import * as logger from 'winston'
import {autoSerialize, Serializable, serialize, serializeHelp} from "./serializeDecorator"
import * as EventEmitter from 'events'
import {PlayerModel} from '../database/models/player'
import {ConsumeRecordModel} from '../database/models/consumeRecord'
import Club from '../database/models/club'
import ClubMember from '../database/models/clubMember'
import {once} from "./onceDecorator";

function eqlModelId(s1, s2) {
  try {
    return s1.toString() === s2.toString()
  } catch (e) {
    return false
  }
}

export interface SimplePlayer {
  _id: string
  model: any
  room: RoomBase
  seatIndex: number

  sendMessage(name: string, message: any): void

  isRobot(): boolean

  addGold(gold: number)

  on(event: string, func)

  removeAllListeners(): void
}


export interface IRoom extends EventEmitter {
  players: SimplePlayer[]
  creator: SimplePlayer
  creatorName: string

  gameState?: any
  clubMode: boolean
  _id: string

  disconnectCallback(json: any): void

  broadcast(messageName: string, message: any)

  broadcastRejoin(player: SimplePlayer): void

  indexOf(player: SimplePlayer): number


  isFull(player: SimplePlayer): boolean

  join(player: SimplePlayer)

  leave(player: SimplePlayer)

  canJoin(p: SimplePlayer): boolean

  ready(p: SimplePlayer): void

  nextGame(p: SimplePlayer): boolean

  onRequestDissolve(player: SimplePlayer)

  onAgreeDissolve(player: SimplePlayer)

  onDisagreeDissolve(player: SimplePlayer)

  dissolve(player: SimplePlayer)

  forceDissolve()
}


export interface ITable {
  destroy(): void

  start(): void
}

export interface IGame {
  juIndex: number
  juShu: number
  rule: any

  isAllOver(): boolean

  startGame(room: RoomBase): ITable
}

export const clubOwnerIsRich = async (clubId: string, rule, Room) => {
  if (!clubId) {
    return false
  }

  const club = await Club.findOne({_id: clubId}).populate('owner')
  if (!club || !club.owner) {
    return false
  }
  const fee = Room.roomFee(rule)

  return club.owner.gem >= fee;

}

export const playerInClub = async (clubShortId: string, playerId: string, gameType: string) => {
  if (!clubShortId) {
    return false
  }
  const club = await Club.findOne({shortId: clubShortId, gameType: gameType})
  if (!club) {
    return false
  }

  if (club.owner === playerId) {
    return true;
  }

  return ClubMember.findOne({club: club._id, member: playerId}).exec()
}

export abstract class RoomBase extends EventEmitter implements IRoom, Serializable {

  @autoSerialize
  dissolveTime: number

  dissolveTimeout: NodeJS.Timer

  @autoSerialize
  players: SimplePlayer[]

  @serialize
  playersOrder: any[]

  @autoSerialize
  readyPlayers: string[]

  @autoSerialize
  snapshot: any[]

  @autoSerialize
  disconnected: [string, number][]

  @autoSerialize
  scoreMap: any

  @serialize
  gameState: ITable


  @serialize
  game: IGame

  disconnectCallback: (player) => void

  @autoSerialize
  isPublic: boolean
  @autoSerialize
  charged: boolean
  charge: () => void

  capacity: number

  listenOn: string[]
  isPlayAgain: boolean = false


  @autoSerialize
  ownerId: string
  @autoSerialize
  creatorName: any

  @autoSerialize
  creator: any

  // noinspection TsLint
  @autoSerialize
  _id: string
  @autoSerialize
  uid: string
  @autoSerialize
  roomState: string = ''

  @autoSerialize
  gameRule: any


  @autoSerialize
  dissolveReqInfo: Array<{ name: string, _id: string, type: string }> = []

  @autoSerialize
  clubId: number

  @autoSerialize
  clubMode: boolean = false

  @autoSerialize
  clubOwner: any
  protected autoDissolveTimer: NodeJS.Timer

  abstract initScore(player)

  broadcast(name, message, except?) {
    for (let i = 0; i < this.players.length; ++i) {
      const player = this.players[i]
      if (player && player !== except) {
        player.sendMessage(name, message)
      }
    }
  }

  async setClub(clubId, clubOwner) {
    this.clubId = clubId;
    this.clubOwner = clubOwner

    this.clubMode = true;
    if (this.gameRule.clubPersonalRoom === false) {
      this.charge = () => {
        this.charged = true;
      }
      await this.chargeClubOwner()
    }
  }

  abstract privateRoomFee(rule: any): number

  canJoin(player) {
    if (!player) {
      return false
    }

    if (this.indexOf(player) >= 0) {
      return true
    }

    return this.players.filter(x => x).length + this.disconnected.length < this.capacity
  }

  mergeOrder() {
    for (let i = 0; i < this.players.length; i++) {
      if (this.players[i]) {
        this.playersOrder[i] = this.players[i]
      }
    }
  }

  getScore(player) {
    return this.scoreMap[player._id]
  }

  getScoreBy(playerId) {
    return this.scoreMap[playerId] || 0
  }

  indexOf(player) {
    return this.playersOrder.findIndex(playerOrder => playerOrder && player && playerOrder._id == player._id)
  }

  isReadyPlayer(playerId) {
    for (const readyPlayerId of this.readyPlayers) {
      if (readyPlayerId === playerId) {
        return true
      }
    }
    return false
  }

  ready(player) {
    if (this.isReadyPlayer(player._id)) {
      return
    }

    if (this.gameState) {
      return
    }

    this.readyPlayers.push(player._id)
    this.broadcast('room/playerReady', {
      index: this.players.indexOf(player),
      readyPlayers: this.readyPlayers
    })

    if (this.allReady) {
      if (!this.game.isAllOver())
        this.startGame()
    }
  }

  clearReady() {
    this.readyPlayers = []
  }

  get allReady() {
    if (this.game.juIndex >= 1) {
      return this.readyPlayers.length === this.playersOrder.filter(p => p).length;
    }
    return this.readyPlayers.length === this.capacity
  }

  startGame() {
    this.readyPlayers = this.players.filter(p => p).map(x => x._id)
    this.playersOrder = this.players.slice()
    this.snapshot = this.players.slice()
    this.isPlayAgain = false

    this.destroyOldGame()
    this.startNewGame()
  }


  destroyOldGame() {
    if (this.gameState) {
      this.gameState.destroy()
    }
  }

  startNewGame() {
    this.destroyOldGame()
    const gameState = this.game.startGame(this)
    this.gameState = gameState
    this.broadcastStartGame()
    gameState.start()
  }

  broadcastStartGame() {
    this.broadcast('room/startGame', {})
  }


  join(newJoinPlayer) {
    const isReconnect = this.indexOf(newJoinPlayer) >= 0

    if (isReconnect || this.disconnected.find(x => x[0] === newJoinPlayer._id)) {
      return this.reconnect(newJoinPlayer)
    }
    if (!this.canJoin(newJoinPlayer)) {
      return false
    }
    newJoinPlayer.room = this
    this.listen(newJoinPlayer)

    this.arrangePos(newJoinPlayer, isReconnect)

    this.mergeOrder()

    this.initScore(newJoinPlayer)

    this.emit('join')
    this.announcePlayerJoin(newJoinPlayer)

    this.pushToSnapshot(newJoinPlayer)

    this.joinInHalf(newJoinPlayer);
    return true
  }

  joinInHalf(newJoinPlayer) {
    if (this.rule.clubPersonalRoom === false) {
      return
    }
    if (this.rule.share && this.game.juIndex > 1) {
      const fee = this.privateRoomFee(this.rule)

      this.payUseGem(newJoinPlayer, fee)
    }
  }

  leave(player) {
    if (!player) return false

    if (this.indexOf(player) < 0) {
      return true
    }

    if (this.game.juIndex > 0 && !this.game.isAllOver()) return false

    this.removePlayer(player)
    this.removeOrder(player)

    player.room = null

    this.broadcast('room/leave', {_id: player._id})
    this.cancelReady(player._id)

    this.emit('leave', {_id: player._id})
    if (this.isEmpty()) {
      this.emit('empty', this.disconnected);
      this.readyPlayers = [];
    }

    this.removeReadyPlayer(player._id);
    this.clearScore(player._id);
    return true
  }

  clearScore(playerId) {
    if (!this.isPublic) {
      // delete this.scoreMap[playerId];
    }
  }

  cancelReady(playerId: string) {
    const index = this.readyPlayers.indexOf(playerId)
    if (index >= 0) {
      this.readyPlayers.splice(index, 1)
      return true
    }
    return false
  }

  removeReadyPlayer(playerId: string) {
    let index = this.readyPlayers.indexOf(playerId)
    if (index >= 0) {
      this.readyPlayers.splice(index, 1)
      return true
    }
    return false
  }

  isEmpty() {
    return this.inRoomPlayers.length + this.disconnected.length === 0
  }

  get inRoomPlayers() {
    return this.players.filter(p => p)
  }

  nextGame(thePlayer) {
    if (!this.isPublic && this.game.juShu <= 0) {
      thePlayer.sendMessage('room/join-fail', {reason: '牌局已经结束.'})
      return
    }
    if (this.indexOf(thePlayer) == -1) {
      thePlayer.sendMessage('room/join-fail', {reason: '你已经不属于这个房间.'})
      return false
    }

    this.announcePlayerJoin(thePlayer)
    // this.evictFromOldTable(thePlayer)

    return true
  }


  protected removeOrder(player: SimplePlayer) {
    for (let i = 0; i < this.playersOrder.length; i++) {
      const po = this.playersOrder[i]
      if (po && eqlModelId(po, player)) {
        this.playersOrder[i] = null
      }
    }
  }


  abstract reconnect(reconnectPlayer: SimplePlayer): void

  protected pushToSnapshot(newJoinPlayer: any) {
    if (~~this.snapshot.indexOf(newJoinPlayer)) {
      this.snapshot.push(newJoinPlayer)
    }
  }

  arrangePos(player, reconnect?) {
    if (reconnect) {
      const indexForPlayer = this.indexOf(player)
      if (indexForPlayer < 0) {
        this.arrangePos(player)
      }

      this.players[indexForPlayer] = player
      return
    }
    for (let i = 0; i < this.players.length; i++) {
      if (this.players[i] == null && this.playersOrder[i] == null) {
        this.players[i] = player
        break
      }
    }
  }

  removePlayer(leaver) {
    const index = this.players.indexOf(leaver)
    if (index > -1) {
      this.players[index] = null
    }
  }


  announcePlayerJoin(newJoinPlayer) {
    this.broadcast('room/join', this.joinMessageFor(newJoinPlayer))
    this.players
      .map((p, index) => {
        return p || this.playersOrder[index]
      })
      .filter(x => x !== null && x.model._id !== newJoinPlayer.model._id)
      .forEach(
        alreadyInRoomPlayer =>
          newJoinPlayer.sendMessage('room/join', this.joinMessageFor(alreadyInRoomPlayer))
      )
  }


  broadcastRejoin(reconnectPlayer) {
    if (!reconnectPlayer) {
      return
    }
    this.broadcast('room/rejoin', this.joinMessageFor(reconnectPlayer))
  }

  abstract joinMessageFor(newJoinPlayer): any

  isFull(player) {
    if (this.players.filter(x => x != null).length >= this.capacity) {
      return true
    }
    if (this.readyPlayers.length >= this.capacity) {
      return !(player && this.isReadyPlayer(player._id))
    }
    return false
  }

  chargeCreator() {
    if (this.charged) return

    this.charged = true
    const createRoomNeed = this.privateRoomFee(this.rule)
    this.payUseGem(this.creator, createRoomNeed)
  }

  getPlayerById(id: string) {
    return this.players.find(p => p && p._id === id)
  }

  chargeAllPlayers() {
    if (this.charged) return

    this.charged = true
    const fee = this.privateRoomFee(this.rule)

    for (const player of this.snapshot) {
      this.payUseGem(player, fee)
    }
  }

  async chargeClubOwner() {
    const fee = this.privateRoomFee(this.rule)

    PlayerModel.update({_id: this.clubOwner._id},
      {
        $inc: {
          gem: -fee,
        },
      },
      (err) => {
        if (err) {
          logger.error(this.clubOwner._id, err)
        }
      })

    this.clubOwner.sendMessage('resource/createRoomUsedGem', {
      createRoomNeed: fee
    })
  }

  @once
  async refundClubOwner() {
    if (!this.clubMode) return
    if (this.charged) return
    if (this.gameRule.clubPersonalRoom === false) {
    } else {
      return
    }


    const fee = this.privateRoomFee(this.rule)

    PlayerModel.update({_id: this.clubOwner._id},
      {
        $inc: {
          gem: fee,
        },
      },
      (err) => {
        if (err) {
          logger.error(this.clubOwner, err)
        }
      })

    this.clubOwner.sendMessage('resource/createRoomUsedGem', {
      createRoomNeed: -fee
    })
  }

  abstract listen(player)

  get rule() {
    return this.game.rule
  }

  toJSON() {
    return serializeHelp(this)
  }

  initDissolveByPlayer(player: SimplePlayer) {
    this.dissolveReqInfo = []
    this.dissolveTime = Date.now();
    this.dissolveReqInfo.push({
      type: 'originator',
      name: player.model.name,
      _id: player.model._id
    })
    for (let i = 0; i < this.players.length; i++) {
      const pp = this.players[i]
      if (pp && pp.isRobot()) {
        this.dissolveReqInfo.push({
          type: 'agree',
          name: pp.model.name,
          _id: pp.model._id
        })
      } else if (pp && pp !== player) {
        this.dissolveReqInfo.push({
          type: 'waitConfirm',
          name: pp.model.name,
          _id: pp.model._id
        })
      }
    }
    for (let i = 0; i < this.disconnected.length; i++) {
      const pp = this.disconnected[i]
      this.snapshot.forEach(player => {
          if (player && player.model._id === pp[0]) {
            this.dissolveReqInfo.push({
              type: 'offline',
              name: player.model.name,
              _id: player.model._id
            })
          }
        }
      )
    }
    return this.dissolveReqInfo
  }


  canDissolve() {
    if (this.dissolveReqInfo.length === 0) {
      return false
    }

    const onLinePlayer = this.dissolveReqInfo
      .filter(reqInfo => {
        const id = reqInfo._id
        return !this.disconnected.some(item => item[0] === id)
      })
    const agreeReqs = onLinePlayer.filter(reqInfo => reqInfo.type === 'agree'
      || reqInfo.type === 'originator' || reqInfo.type === 'agree_offline')

    if (onLinePlayer.length <= 2) {
      if (agreeReqs.length === 2) {
        return true;
      }
      return false;
    }

    return agreeReqs.length > 0 && agreeReqs.length + 1 >= onLinePlayer.length
  }


  onRequestDissolve(player) {
    const dissolveInfo = this.initDissolveByPlayer(player)
    this.broadcast('room/dissolveReq',
      {dissolveReqInfo: dissolveInfo, startTime: this.dissolveTime})
    if (this.canDissolve()) {
      this.forceDissolve()
      return
    }

    if (!this.dissolveTimeout) {
      this.roomState = 'dissolve'
      this.dissolveTimeout = setTimeout(() => {
        this.forceDissolve()
      }, 180 * 1000)
    }

    return true
  }

  dissolveOverMassage() {
    return this.allOverMessage()
  }

  async forceDissolve() {
    clearTimeout(this.autoDissolveTimer)
    this.recordDrawGameScore()
    const allOverMessage = this.dissolveOverMassage()

    await this.refundClubOwner();
    clearTimeout(this.dissolveTimeout)
    this.roomState = ''
    this.dissolveTimeout = null
    this.players
      .filter(p => p)
      .forEach(player => {
        player.sendMessage('room/dissolve', allOverMessage)
        player.room = null
      })
    this.players.fill(null)
    this.dissolveAndDestroyTable()
    this.emit('empty', this.disconnected.map(x => x[0]))
    return true
  }

  dissolveAndDestroyTable() {
    if (this.gameState) {
      this.gameState.destroy()
    }
  }

  onAgreeDissolve(player) {
    const item = this.dissolveReqInfo.find((x) => {
      return x.name === player.model.name
    })
    if (item) {
      item.type = 'agree'
    }
    this.broadcast('room/dissolveReq', {dissolveReqInfo: this.dissolveReqInfo})

    if (this.canDissolve()) {
      this.forceDissolve()
      return
    }
    return true
  }

  onDisagreeDissolve(player) {

    const item = this.dissolveReqInfo.find((x) => {
      return x.name === player.model.name
    })
    if (item) {
      item.type = 'disAgree'
      clearTimeout(this.dissolveTimeout)
      this.roomState = ''
      this.dissolveTimeout = null
    }
    this.broadcast('room/dissolveReq',
      {dissolveReqInfo: this.dissolveReqInfo})
    return true
  }

  async dissolve(roomCreator) {
    if (roomCreator._id !== this.ownerId) {
      roomCreator.sendMessage('room/dissolveReply', {errorCode: 1})
      return false
    }

    this.autoDissolveTimer && clearTimeout(this.autoDissolveTimer)
    await this.refundClubOwner();
    this.dissolveAndDestroyTable()

    roomCreator.sendMessage('room/dissolveReply', {errorCode: 0})
    roomCreator.room = null
    this.players.forEach(player => {
      if (player && player !== roomCreator) {
        player.sendMessage('room/dissolve', {})
        player.room = null
      }
    })
    this.emit('empty', this.disconnected.map(x => x[0]))
    this.players.fill(null)
    return true
  }

  payUseGem(player, toPay: number, note: string = '') {
    const condition = {_id: player.model._id}
    const update = {$inc: {gem: -toPay}}
    const options = {new: true}
    const callback = (err, newDoc) => {
      if (err) {
        logger.error(player.model, err)
      } else {
        if (newDoc) {
          player.model.gem = newDoc.gem
          player.sendMessage('resource/createRoomUsedGem', {
            createRoomNeed: toPay
          })
          new ConsumeRecordModel({
            player: player.model._id,
            gem: toPay,
            note: `${note}=>${newDoc.gem}/${newDoc.gold}`
          }).save()
        }
      }
    }

    PlayerModel.findOneAndUpdate(condition, update, options, callback)
  }

  protected abstract allOverMessage(): any

  protected abstract recordDrawGameScore(): any

  abstract gameOver(states: any, winnerPlayerId: string)

}
